﻿/*******************************************************************
 *     JameE
 *******************************************************************
  Copyright 2010 Clifford Champion and JameE contributors.
  
  This file is part of JameE.
  
  JameE is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  
  JameE is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with JameE.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************/
 
/// <reference path="~/Base/Classer.js" />
/// <reference path="~/Base/IncludeLoader.js" />

$class(
"JameE.Engine.Scene",
{
	/////////////////////
	// FIELDS
	/////////////
	_arr_oChildren: null,
	_hash_oChildrenById: null,

	_oCameraPosition: null,
	_fCameraZoom: null,
	_fCameraAspectRatio: null,
	_oCameraMatrix: null, // updated manually.
	_oCameraMatrixInverse: null, // updated manually.

	_oBroadPhase: null,
	_bDrawCoordinateSystem: false,

	_oPostOffice: null,


	/////////////////////
	// CONSTRUCTOR
	/////////////
	_ctor: function (args) {

		// camera and coordinate system
		this._oCameraPosition = $argument("cameraPosition", "Vector3") || $vector3();
		this._fCameraZoom = $argument("cameraZoom", "number") || (1 * 0.005);
		this._fCameraAspectRatio = $argument("cameraAspectRatio", "number") || 1.3333;
		this._bDrawCoordinateSystem = $argument("drawCoordinateSystem", "boolean") || false;
		this._oCameraMatrix = new JameE.Math.Matrix(3, 3);
		this._oCameraMatrixInverse = new JameE.Math.Matrix(3, 3);
		this._updateCameraMatrices();

		// time
		this._fTotalTimePrev = new Date().getTime();

		// children and collision detection
		this._oBroadPhase = new JameE.Engine.BroadPhase();
		this._arr_oChildren = [];
		this._hash_oChildrenById = {};

		// message
		this._oPostOffice = new JameE.Engine.PostOffice();
	},


	/////////////////////
	// METHODS: children
	/////////////
	add: function (arr_oSceneObjects) {

		if ($typeof(arr_oSceneObjects) !== "array") {
			arr_oSceneObjects = [arr_oSceneObjects];
		}

		// add to internal list
		for (var i = arr_oSceneObjects.length - 1; i >= 0; --i) {
			var oSceneObject = arr_oSceneObjects[i];
			// todo: inheritence support for $require_type(oSceneObject, "SceneObject");
			var strSceneObjectId = oSceneObject.getId();

			var indexThatIsNull = -1;

			// does it exist already? and track down a null entry
			for (var j = 0; j < this._arr_oChildren.length; ++j) {
				if (this._arr_oChildren[j] === oSceneObject) {
					throw new Error("One or more given objects already exists in the scene.");
				}

				if (indexThatIsNull == -1 && this._arr_oChildren[j] == null) {
					indexThatIsNull = j;
				}
			}

			// does an object by the same id exist already?
			if (strSceneObjectId !== null
				&& this._hash_oChildrenById.hasOwnProperty(strSceneObjectId)) {
				throw new Error("An object with id '" + strSceneObjectId + "' already exists in the scene.");
			}

			// add to flat list.
			if (indexThatIsNull !== -1) {
				this._arr_oChildren[indexThatIsNull] = oSceneObject;
			}
			else {
				this._arr_oChildren.push(oSceneObject);
			}

			// add to hash
			this._hash_oChildrenById[strSceneObjectId] = oSceneObject;
		}

		// add set to broad phase
		this._oBroadPhase.add(arr_oSceneObjects);

		// notify objects of new scene
		for (var i = arr_oSceneObjects.length - 1; i >= 0; --i) {
			var oSceneObject = arr_oSceneObjects[i];
			oSceneObject.addedToScene(this);
		}

		// sort by z order (for rendering)
		this._arr_oChildren.sort(function (a, b) {
			if (a === null) {
				return -1;
			}
			if (b === null) {
				return -1;
			}

			var fA = a.getPositionZ();
			var fB = b.getPositionZ();
			if (fA < fB) {
				return -1;
			}
			else if (fA === fB) {
				return 0;
			}
			else {
				return 1;
			}
		});
	},

	remove: function (oSceneObject) {
		// remove from flat list
		for (var i = 0; i < this._arr_oChildren.length; ++i) {
			if (this._arr_oChildren[i] == oSceneObject) {
				this._arr_oChildren[i] = null;
				break;
			}
		}

		// remove from hash
		var strId = oSceneObject.getId();
		if (strId !== null) {
			delete this._hash_oChildrenById[strId];
		}

		// remove from broad phase
		this._oBroadPhase.remove(oSceneObject);

		// notify object
		oSceneObject.removedFromScene(this);
	},

	find: function (strId) {
		/// <summary>Returns the scene object with the given id.</summary>
		$require_type(strId, "string");

		if (this._hash_oChildrenById.hasOwnProperty(strId)) {
			return this._hash_oChildrenById[strId];
		}
		else {
			return null;
		}
	},

	forEachSceneObject: function (fnCallback) {
		$require_type(fnCallback, "function");

		var arr_oChildren = this._arr_oChildren;
		var iNumChildren = arr_oChildren.length;
		for (var i = 0; i < iNumChildren; ++i) {
			var oTmp = arr_oChildren[i];
			if (oTmp === null) {
				continue;
			}
			var bResult = fnCallback(oTmp) || true;
			if (bResult === false) {
				break;
			}
		}
	},

	/////////////////////
	// METHODS: step cycle
	/////////////
	update: function (iDelta, iTime) {
		// go through each object and update it
		for (var i = 0; i < this._arr_oChildren.length; ++i) {
			var oThing = this._arr_oChildren[i];
			if (oThing !== null && typeof (oThing.update) === "function") {
				oThing.update(iDelta, iTime);
			}
		}
	},

	draw: function (oContext) {

		oContext.save();

		// clear scene
		oContext.clearRect(-1, -1, 2, 2);

		// push camera transform to stack
		oContext.transform(
			this._oCameraMatrix.getValue(0, 0),
			this._oCameraMatrix.getValue(1, 0),
			this._oCameraMatrix.getValue(0, 1),
			this._oCameraMatrix.getValue(1, 1),
			this._oCameraMatrix.getValue(0, 2),
			this._oCameraMatrix.getValue(1, 2)
		);

		// go through each object and draw it
		for (var i = 0; i < this._arr_oChildren.length; ++i) {
			var oThing = this._arr_oChildren[i];
			if (oThing != null) {
				oThing.draw(oContext);
			}
		}

		// draw coordinate system
		if (this._bDrawCoordinateSystem) {
			this._drawCoordinateSystem(oContext);
		}

		oContext.restore();
	},


	/////////////////////
	// METHODS
	/////////////
	_updateCameraMatrices: function () {

		//////
		// calculate camera matrix
		var oT1 = $matrix(3, 3);
		oT1.setIdentity();
		oT1.setValue(0, 0, 1.0 / this._fCameraAspectRatio);
		oT1.setValue(1, 1, 1.0);

		var oT2 = $matrix(3, 3);
		oT2.setIdentity();
		oT2.setValue(0, 0, this._fCameraZoom);
		oT2.setValue(1, 1, this._fCameraZoom);

		var oT3 = $matrix(3, 3);
		oT3.setIdentity();
		oT3.setValue(0, 2, this._oCameraPosition.fX);
		oT3.setValue(1, 2, this._oCameraPosition.fY);

		var oComposite = oT1.multiply(oT2).multiply(oT3);

		this._oCameraMatrix = oComposite;


		//////
		// calculate inverse camera matrix
		var oT1inv = $matrix(3, 3);
		oT1inv.setIdentity();
		oT1inv.setValue(0, 0, this._fCameraAspectRatio);
		oT1inv.setValue(1, 1, 1.0);

		var oT2inv = $matrix(3, 3);
		oT2inv.setIdentity();
		oT2inv.setValue(0, 0, 1.0 / this._fCameraZoom);
		oT2inv.setValue(1, 1, 1.0 / this._fCameraZoom);

		var oT3inv = $matrix(3, 3);
		oT3inv.setIdentity();
		oT3inv.setValue(0, 2, -this._oCameraPosition.fX);
		oT3inv.setValue(1, 2, -this._oCameraPosition.fY);

		var oCompositeInv = oT3inv.multiply(oT2inv).multiply(oT1inv);

		this._oCameraMatrixInverse = oCompositeInv;
	},

	_drawCoordinateSystem: function (oContext) {
		oContext.save();

		//////
		// draw lines
		oContext.lineWidth = 1;
		oContext.strokeStyle = "rgba(0,0,0,128)";

		// +x
		oContext.beginPath();
		oContext.moveTo(0, 0);
		oContext.lineTo(100, 0);
		oContext.closePath();
		oContext.stroke();

		// -x
		oContext.beginPath();
		oContext.moveTo(0, 0);
		oContext.lineTo(-100, 0);
		oContext.closePath();
		oContext.stroke();

		// +y
		oContext.beginPath();
		oContext.moveTo(0, 0);
		oContext.lineTo(0, 100);
		oContext.closePath();
		oContext.stroke();

		// -y
		oContext.beginPath();
		oContext.moveTo(0, 0);
		oContext.lineTo(0, -100);
		oContext.closePath();
		oContext.stroke();


		//////
		// draw text
		oContext.font = "8px Consolas";
		oContext.fillText("0", 2, -2);
		oContext.fillText("+100x", 100, -2);
		oContext.fillText("-100x", -100, -2);
		oContext.fillText("+100y", 2, 100);
		oContext.fillText("-100y", 2, -100);


		oContext.restore();
	},


	/////////////////////
	// METHODS: messaging
	/////////////
	message: function (oMessage) {
		// deliver to post office
		this._oPostOffice.message(oMessage);

		// was it a mouse event? generate and send transformed version
		var iMsg = oMessage.getMessage();
		switch (iMsg) {
			case JameE.Engine.MessageTypes.MOUSE_DOWN_RAW:
			case JameE.Engine.MessageTypes.MOUSE_UP_RAW:
			case JameE.Engine.MessageTypes.MOUSE_MOVE_RAW:
			case JameE.Engine.MessageTypes.MOUSE_CLICK_RAW:
				this._onMessage_MouseXXX(oMessage);
				break;
		}
	},

	attachMessageListener: function (iMessageType, fnListener) {
		this._oPostOffice.attachMessageListener(iMessageType, fnListener);
	},

	getPostOffice: function () {
		return this._oPostOffice;
	},

	_cloneMessageAndTransformPoint: function (oMessage, iNewMessageType) {
		/// <summary>Returns a clone of the message, with 
		/// it's x,y coordinates transformed from normalized
		/// screen-space to world-space.</summary>

		var oClonedMessage = oMessage.clone();
		var oPoint = $vector3(oClonedMessage.getData().x, oClonedMessage.getData().y);
		var oPointWorld = oPoint.transform(this._oCameraMatrixInverse);
		oClonedMessage.getData().x = oPointWorld.fX;
		oClonedMessage.getData().y = oPointWorld.fY;
		oClonedMessage._iMessage = iNewMessageType;
		return oClonedMessage;
	},

	_onMessage_MouseXXX: function (oMessage) {

		// transform to world coords
		var iNewMessageType = oMessage.getMessage();
		switch (iNewMessageType) {
			case JameE.Engine.MessageTypes.MOUSE_MOVE_RAW:
				iNewMessageType = JameE.Engine.MessageTypes.MOUSE_MOVE_GLOBAL;
				break;
			case JameE.Engine.MessageTypes.MOUSE_DOWN_RAW:
				iNewMessageType = JameE.Engine.MessageTypes.MOUSE_DOWN_GLOBAL;
				break;
			case JameE.Engine.MessageTypes.MOUSE_UP_RAW:
				iNewMessageType = JameE.Engine.MessageTypes.MOUSE_UP_GLOBAL;
				break;
			case JameE.Engine.MessageTypes.MOUSE_CLICK_RAW:
				iNewMessageType = JameE.Engine.MessageTypes.MOUSE_CLICK_GLOBAL;
				break;
		}
		oMessage = this._cloneMessageAndTransformPoint(oMessage, iNewMessageType);

		// send transformed message
		this._oPostOffice.message(oMessage);

		// hit test
		if (oMessage.getMessage() === JameE.Engine.MessageTypes.MOUSE_CLICK_GLOBAL) {
			var oCoords = $vector3(oMessage.getData().x, oMessage.getData().y);
			var arr_oHits = this._oBroadPhase.castPoint(oCoords);
			if (arr_oHits.length > 0) {
				for (var k in arr_oHits) {

					var oHitMessage
						= new JameE.Engine.Message(
							{
								message: JameE.Engine.MessageTypes.MOUSE_TOUCH,
								to: arr_oHits[k]
							}
						)
					;

					this._oPostOffice.message(oHitMessage);
				}
			}
		}
	}
});

$include_notify("Engine/Scene.js");

